﻿using System;

using System.Collections.Generic;
using System.Text;
using Silvermoon.Windows;
using Silvermoon.Controls.Base;
using System.Drawing;
using Silvermoon.Controls.Interfaces;
using Silvermoon.Animations;
using System.Diagnostics;
using System.Collections.ObjectModel;
using Silvermoon.Utils;
using Silvermoon.UI;
using Silvermoon.Controls.Navigation;
using System.Collections;

namespace Silvermoon.Controls
{
    /// <summary>
    /// Control to display a collection of data.
    /// </summary>
    public abstract class PickerBase : ScrollViewer, IItemHost
    {
        #region fields

        const int CYCLIC = 64;
        const int RESTING = 128;

        public static Command PickerTapCommand = new Command();

        private List<object> items;
        private float siblingTransition = 1f;
        private IList itemsSource;
        private int centeredItemIndex = -1;
        private int selectedItemIndex = -1;
        private int itemHeight = 80;
        private PickerMode mode = PickerMode.Fade;
        private Dictionary<int, Item> cachedItems = new Dictionary<int, Item>();
        private OpacityTransform opacityTransform;

        #endregion
        #region accessors


        public static PropertyAccessor<PickerBase, float> SiblingTransitionProperty = PropertyAccessor.Register<PickerBase, float>(
            "SiblingTransition",
            c => c.siblingTransition,
            (c, v) => c.SiblingTransition = v);


        #endregion
        #region ctors/dtors

        public PickerBase()
            : base()
        {
            bitmask |= CYCLIC;
            opacityTransform = new OpacityTransform();
        }

        protected override void Dispose(bool dispose)
        {
            VisibleItems.Clear();
            this.ItemsSource = null;
            ReleaseItems();
            base.Dispose(dispose);
        }

        #endregion
        #region props

        protected override bool IsScrollbarAvailable { get { return false; } }

        public bool IsCollapsed { get { return siblingTransition == 0f; } }
        public bool IsExpanded { get { return siblingTransition == 1f; } }

        public bool AutoHide
        {
            get { return (mode & PickerMode.AutoHide) != 0; }
            set
            {
                if (AutoHide != value)
                {
                    Mode = value ? mode | PickerMode.AutoHide : mode & ~PickerMode.AutoHide;
                }
            }
        }

        /// <summary>
        /// Gets or sets how neighboured items behave depending on the value of <see cref="P:SiblingTransition"/>.
        /// </summary>
        public PickerMode Mode
        {
            get { return mode; }
            set
            {
                if (mode != value)
                {
                    PickerMode oldValue = mode;
                    mode = value;
                    OnSiblingModeChanged(oldValue);
                }
            }
        }

        /// <summary>
        /// Gets or sets the transition value for neigboured items within a range of [0;1].
        /// See also <seealso cref="P:SiblingMode"/>.
        /// </summary>
        public float SiblingTransition
        {
            get { return siblingTransition; }
            set
            {
                if (siblingTransition != value)
                {
                    float oldValue = siblingTransition;
                    siblingTransition = value;
                    OnSibilingTransitionChanged(oldValue);
                }
            }
        }

        /// <summary>
        /// Gets a collection of objects to display.
        /// </summary>
        public List<object> Items
        {
            get
            {
                if (items == null)
                {
                    items = new List<object>();
                    ItemsSource = items;
                }
                return items;
            }
            set { items = value; }
        }

        public IList ItemsSource
        {
            get { return itemsSource; }
            set
            {
                if (itemsSource != value)
                {
                    var oldValue = itemsSource;
                    itemsSource = value;
                    OnItemsSourceChanged(oldValue);
                }
            }
        }

        /// <summary>
        /// Gets the number of items.
        /// </summary>
        public int Count
        {
            get { return itemsSource != null ? itemsSource.Count : 0; }
        }

        /// <summary>
        /// Not used for Picker.
        /// </summary>
        public override int VirtualHeight
        {
            get { return Height; }
        }

        /// <summary>
        /// Gets or sets the height of each item.
        /// </summary>
        public int ItemHeight
        {
            get { return itemHeight; }
            set
            {
                if (itemHeight != value)
                {
                    int oldValue = itemHeight;
                    itemHeight = value;
                    OnItemHeightChanged(oldValue);
                }
            }
        }

        protected ControlCollection VisibleItems
        {
            get { return VisibleControls; }
        }

        protected bool IsResting
        {
            get { return (bitmask & RESTING) != 0; }
            set
            {
                if (IsResting != value)
                {
                    bitmask = value ? bitmask | RESTING : bitmask & ~RESTING;
                }
            }
        }

        /// <summary>
        /// Gets or sets whether the picker is cyclic, which means that all items are repeated after the end.
        /// </summary>
        public bool IsCyclic
        {
            get { return (bitmask & CYCLIC) != 0; }
            set
            {
                if (IsCyclic != value)
                {
                    bitmask = value ? bitmask | CYCLIC : bitmask & ~CYCLIC;
                    OnCyclicChanged();
                }
            }
        }

        /// <summary>
        /// Gets or sets the selected item.
        /// </summary>
        public object SelectedItem
        {
            get { return GetItemByIndex(selectedItemIndex); }
            set
            {
                SelectedItemIndex = GetItemIndex(value);
            }
        }

        public int SelectedItemIndex
        {
            get { return selectedItemIndex; }
            set
            {
                if (selectedItemIndex != value)
                {
                    int oldValue = selectedItemIndex;
                    selectedItemIndex = value;
                    OnSelectedItemChanged(oldValue);
                }
            }
        }

        protected int CenteredItemIndex
        {
            get { return centeredItemIndex; }
            set
            {
                if (centeredItemIndex != value)
                {
                    int oldValue = centeredItemIndex;
                    centeredItemIndex = value;
                    OnCenteredItemChanged(oldValue);
                }
            }
        }

        #endregion
        #region methods

        public override void OnGotFocus()
        {
            if (AutoHide) TransitionTo(1f);
            base.OnGotFocus();
        }

        public override void OnLostFocus()
        {
            //YOffsetProperty.StopAnimation(this);
            //EnsureRestState(true);
            if (AutoHide)
            {
                if (!this.IsDescendantOf(Screen.FocusedControl))
                {
                    TransitionTo(0f);
                }

            }
            base.OnLostFocus();
        }

        protected virtual void OnSiblingModeChanged(PickerMode oldValue)
        {
            if ((oldValue & PickerMode.AutoHide) != (mode & PickerMode.AutoHide))
            {
                SiblingTransition = IsFocused | !AutoHide ? 1f : 0f;
            }
            Invalidate(InvalidationFlag.Layout);
        }

        protected override void PanInternal(PanEventArgs e)
        {
            if (!e.Handled)
            {
                if (e.VerticalMotion)
                {
                    e.Handled = true;
                    e.Value = new Point(0, yOffset);
                    IsPanning = true;
                }
            }
        }


        /// <summary>
        /// The selected item is only available when the picker is not panning or scrolling or when going to the rest state.
        /// </summary>
        private bool IsSelectionHidden
        {
            get { return IsScrollbarVisible && !IsResting; }
        }


        protected override void OnScrollbarVisibleChanged()
        {
            bool hideSelection = IsSelectionHidden;
            bool visible = IsScrollbarVisible;
            if (visible) CenteredItemIndex = SelectedItemIndex;
            SelectedItemIndex = hideSelection ? -1 : CenteredItemIndex;
            Invalidate(InvalidationFlag.LayoutAppearance);
            if (!visible) EnsureRestState(true);

        }


        protected override void OnScrollingChanged()
        {
            if (!IsScrolling) IsResting = false;
            base.OnScrollingChanged();
        }

        protected virtual void OnSibilingTransitionChanged(float oldValue)
        {
            opacityTransform.Alpha = (int)(siblingTransition * 255f);
            Invalidate(InvalidationFlag.LayoutAppearance);
        }

        protected virtual void OnUnselectedTextColorChanged(Color oldValue)
        {
            Invalidate(InvalidationFlag.Appearance | InvalidationFlag.Color);
        }

        public object GetItemByIndex(int index)
        {
            if (index < 0 || index >= Count) return null;
            return itemsSource[index];
        }

        public int GetIndexFromCyclicIndex(int index)
        {
            int n = Count;
            if (n == 0) return -1;
            index = index % n;
            if (index < 0) index += n;
            return index;
        }

        public int GetCyclicIndex(int index)
        {
            int n = Count;
            if (n == 0) return -1;
            if (index < 0)
            {
                if (IsCyclic)
                {
                    index = (index % n);
                    if (index < 0) index += n;
                }
                else
                {
                    return -1;
                }
            }
            else
            {
                if (index >= n)
                {
                    if (IsCyclic)
                    {
                        index = index % n;
                    }
                    else
                    {
                        return -1;
                    }
                }
            }
            return index;

        }

        private Item GetCachedItem(int index)
        {
            Item cached;
            if (cachedItems.TryGetValue(index, out cached))
            {
                return cached;
            }
            return null;
        }

        private Item GetContainerFromIndex(int index)
        {
            Item cached;
            if (cachedItems.TryGetValue(index, out cached))
            {
                cachedItems.Remove(index);
                return cached;
            }
            return OnGetVirtualItem(GetItem(index), index);
        }

        protected virtual void OnCyclicChanged()
        {
            Invalidate(InvalidationFlag.Layout);
        }

        public int GetItemIndex(object item)
        {
            return itemsSource.IndexOf(item);
        }

        public object GetNextItem(object item)
        {
            if (Count == 0) return null;
            int index = GetItemIndex(item);
            if (index >= 0)
            {
                int max = Count;
                if (++index >= max)
                {
                    if (IsCyclic)
                    {
                        index -= max;
                        return GetItem(index);
                    }
                    else
                    {
                        return null;
                    }
                }
            }
            return null;
        }

        public object GetPreviousItem(object item)
        {
            if (Count == 0) return null;
            int index = GetItemIndex(item);
            if (index >= 0)
            {
                if (--index < 0)
                {
                    if (IsCyclic)
                    {
                        int max = Count;
                        index += max;
                        return GetItem(index);
                    }
                    else
                    {
                        return null;
                    }
                }
            }
            return null;
        }

        public int GetPrevousIndex(int index)
        {
            if (Count == 0) return -1;
            if (--index < 0)
            {
                if (IsCyclic) index += Count;
            }
            return index;
        }

        public int GetNextIndex(int index)
        {
            if (Count == 0) return -1;
            int max = Count;
            if (++index >= max)
            {
                if (IsCyclic) index -= max;
            }
            return index;
        }

        protected virtual void OnItemHeightChanged(int oldValue)
        {
            Invalidate(InvalidationFlag.Layout);
        }

        protected override void OnYOffsetChanged(int oldValue)
        {
            if (!IsCyclic && IsOutOfBounds(yOffset, oldValue))
            {
                YOffsetProperty.StopAnimation(this);
            }

            // don't call base method since they check whether to stop scrolling if YOffset is out of the boundaries.
            Invalidate(InvalidationFlag.Layout);
        }

        private bool IsOutOfBounds(int yOffset, int oldValue)
        {
            yOffset += itemHeight * centeredItemIndex;
            if (yOffset < 0) return true;
            return yOffset > (ItemHeight * Count);
        }

        protected virtual void OnOffsetChanged(int oldValue)
        {
            Invalidate(InvalidationFlag.LayoutAppearance);
        }

        protected virtual void OnCenteredItemChanged(int oldValue)
        {
            Invalidate(InvalidationFlag.LayoutAppearance);
        }


        /// <summary>
        /// Occurs when the <see cref="P:SelectedItem"/> property changes.
        /// </summary>
        protected virtual void OnSelectedItemChanged(int oldValue)
        {
            if (!IsPanning && !IsScrolling) CenteredItemIndex = selectedItemIndex;
            Item oldItem = oldValue >= 0 ? GetCachedItem(oldValue) : null;
            if (oldItem != null) oldItem.IsSelected = false;
            Item newItem = selectedItemIndex >= 0 ? GetCachedItem(selectedItemIndex) : null;
            if (newItem != null) newItem.IsSelected = true;
            //Invalidate(InvalidationFlag.LayoutAppearance);
            RaiseEvent(ItemChangedEvent);
        }


        protected override void OnLayout()
        {
            base.OnLayout();
            if (CheckFlag(InvalidationFlag.Size | InvalidationFlag.Layout))
            {
                LayoutItems();
            }
        }

        private void LayoutItems()
        {
            ControlCollection visibleItems = VisibleItems;
            visibleItems.Clear();
            if (Count == 0) return;

            bool isCyclic = IsCyclic;
            int centeredIndex = this.centeredItemIndex;

            int h = itemHeight;
            int w = Width;
            int height = Height;
            int siblingHeight = (mode & PickerMode.Collapse) != 0 ? (int)(siblingTransition * h) : h;

            int top = (height - h) / 2 - yOffset;

            AddItem(centeredIndex, top, w, h, 255);
            int y = top;
            int index = centeredIndex;

            if (siblingTransition > 0f)
            {
                bool cyclic = isCyclic;
                int opacity = (int)(255 * siblingTransition);
                int min = isCyclic ? centeredIndex - (Height / 2 / h) : 0;

                while (y > 0 && (cyclic || index > min))
                {
                    index--;
                    y -= siblingHeight;
                    AddItem(index, y, w, h, opacity);
                }
                index = centeredIndex + 1;

                int max = isCyclic ? centeredIndex + (Height / 2 / h) + 1 : Count;

                y = top + siblingHeight;
                while (y < height && (cyclic || index < max))
                {
                    AddItem(index, y, w, h, opacity);
                    index++;
                    y += siblingHeight;
                }

            }
            AddItemsToCache();
        }

        private object GetItem(int index)
        {
            if (itemsSource == null) return null;
            if (index < 0 || index >= Count) return null;
            return itemsSource[index];
        }

        private void AddItemsToCache()
        {
            ReleaseItems();
            cachedItems.Clear();
            Control c = VisibleItems.First;
            while (c != null)
            {
                Item tile = c as Item;
                cachedItems.Add(tile.ItemIndex, tile);
                c = c.NextControl;
            }
        }

        private void ReleaseItems()
        {
            foreach (var pair in cachedItems)
            {
                int index = pair.Key;
                Item container = pair.Value;
                container.Release();
                container.Transformation -= opacityTransform;
                object value = GetItem(index);
                OnReleaseVirtualItem(container, value, index);
            }
        }

        private void AddItem(int index, int y, int w, int h, int opacity)
        {
            int miny = 0;
            int maxy = Height;
            if (!(((y + h) <= miny) || (y >= maxy)))
            {
                index = GetCyclicIndex(index);
                Item item = index >= 0 ? GetContainerFromIndex(index) : null;
                if (item != null)
                {
                    Rectangle bounds = new Rectangle(0, y, w, h);
                    //item.Opacity = opacity;
                    item.Bounds = item.Margin.LayoutChild(bounds);
                    VisibleItems.AddFirst(item);
                    item.Attach(index, GetItem(index), this);
                    item.Transformation -= opacityTransform;
                    if (opacity != 255) item.Transformation += opacityTransform;
                }
            }
        }

        public override void EnsureRestState(bool animated, IEasingFunction ease)
        {
            YOffsetProperty.StopAnimation(this);
            int y = YOffset;
            if (y != 0)
            {
                int targetIndex = GetCenteredItemIndex();
                SetSelectedItemIndexAnimated(targetIndex);
            }
        }

        private int GetCenteredItemIndex()
        {
            int h = itemHeight;
            int h2 = h / 2;
            int y = YOffset + Math.Sign(YOffset) * h2;

            int targetIndex = y / h + centeredItemIndex;
            if (!IsCyclic)
            {
                if (targetIndex < 0) targetIndex = 0;
                else if (targetIndex >= Count) targetIndex = Count - 1;
            }
            return targetIndex;
        }

        public void OnCollectionChanged()
        {
            Invalidate(InvalidationFlag.Layout);
        }

        protected internal override void OnCommand(CommandEventArgs args)
        {
            if (args.Handled) return;
            if (args.Command == PickerTapCommand)
            {
                Item tile = args.Owner as Item;
                OnTapItem(tile);
                args.Handled = true;
            }
            else
            {
                base.OnCommand(args);
            }
        }

        public virtual void OnTapItem(Item item)
        {
            if (AutoHide)
            {
                if (!IsExpanded) TransitionTo(1f);
                else if (item.ItemIndex == selectedItemIndex && YOffset == 0)
                {
                    TransitionTo(0f);
                    return;
                }
            }
            int index = GetPhysicalIndexFromItem(item);
            SetSelectedItemIndexAnimated(index);
        }

        protected internal override void OnBeginPan(PanEventArgs e)
        {
            if (!IsExpanded) TransitionTo(1f);
            base.OnBeginPan(e);
        }

        /// <summary>
        /// When an item is tapped, the index does not say whether to scroll up or down if the picker is cyclic.
        /// Therefore this information is taken from the location of the item relative to the centered item.
        /// </summary>
        private int GetPhysicalIndexFromItem(Item item)
        {
            int h = itemHeight;
            int top = (Height - h) / 2 - yOffset;
            return (item.Y - item.Margin.TopZero - top) / h + centeredItemIndex; // note: substracting Margin.TopZero is necassary!
        }

        /// <summary>
        /// Animates to the specified index.
        /// </summary>
        /// <param name="index">Index of item to animate to.</param>
        protected virtual void SetSelectedItemIndexAnimated(int index)
        {
            if (GetCyclicIndex(index) == selectedItemIndex && YOffset == 0) return;

            int deltaOffset = this.YOffset - itemHeight * (index - centeredItemIndex);

            index = GetCyclicIndex(index);
            SelectedItemIndex = index;
            CenteredItemIndex = index;
            YOffsetProperty.StopAnimation(this);
            YOffset = deltaOffset;
            if (deltaOffset != 0)
            {
                IsResting = true;
                GoToYOffset(0);
            }
        }

        /// <summary>
        /// Sets the selected item index.
        /// </summary>
        /// <param name="index">Index to set. If the index is out of range, the selected index is extrapolated.</param>
        /// <param name="animated">true to set in an animation, otherwise false.</param>
        public void SetItemIndex(int index, bool animated)
        {
            if (animated)
            {
                if (IsCyclic)
                {
                    int delta = centeredItemIndex - index;
                    int max = Count / 2;
                    if (delta > max)
                    {
                        index -= Count;
                    }
                    else if (delta < -max)
                    {
                        index += Count;
                    }
                }
                SetSelectedItemIndexAnimated(index);
            }
            else
            {
                index = GetCyclicIndex(index);
                SelectedItemIndex = index;
            }
        }



        public void TransitionTo(float value)
        {
            if (value < 0f) value = 0f; else if (value > 1f) value = 1f;
            FloatTransition tranistion = new FloatTransition
            {
                To = value,
                Duration = 500,
                EasingFunction = SineEase.InOut,
                Tracking = TrackMode.Keep
            };
            tranistion.Completed += OnTransitionCompleted;
            SiblingTransitionProperty.BeginAnimation(this, tranistion);
        }

        protected virtual void OnTransitionCompleted(object sender, EventArgs e)
        {
            RaiseEvent(TransitionCompletedEvent);
        }

        protected virtual void OnItemsSourceChanged(IList oldValue)
        {
            SelectedItemIndex = 0;
            Invalidate(InvalidationFlag.Layout);
        }

        protected virtual Item OnGetVirtualItem(object value, int index)
        {
            if (GetVirtualItem != null)
            {
                PickerItemEventArgs e = new PickerItemEventArgs(index, value);
                GetVirtualItem(this, e);
                return e.Container;
            }
            Item container = value as Item;
            if (container != null) return container;
            container = ControlPickerContainer.GetNextContainer(value as Control);
            return container;
        }

        protected virtual void OnReleaseVirtualItem(Item item, object value, int index)
        {
            if (ReleaseVirtualItem != null)
            {
                PickerItemEventArgs e = new PickerItemEventArgs(index, value);
                e.Container = item;
                ReleaseVirtualItem(this, e);
            }
        }

        #endregion
        #region IItemHost Members

        public bool IsDynamicItemHeightSupported
        {
            get { return false; }
        }

        public void SetItemHeight(int index, int height)
        {
            throw new NotImplementedException();
        }

        public void BringItemIntoView(int itemIndex, bool animated)
        {
        }

        public void BringItemRectIntoView(int itemIndex, Rectangle rect, bool animated)
        {

        }

        public bool IsItemSelected(Item item)
        {
            return !IsScrolling && selectedItemIndex == item.ItemIndex;
        }

        public Item GetRealizedItem(int index)
        {
            return null;
        }

        #endregion
        #region events

        public static SparseEvent TransitionCompletedEvent = new SparseEvent();

        /// <summary>
        /// Occurs when the <see cref="P:SelectedItem"/> property changes.
        /// </summary>
        public static SparseEvent ItemChangedEvent = new SparseEvent();

        /// <summary>
        /// Occurs when <see cref="ShowSiblings"/> changes.
        /// </summary>
        public static SparseEvent ShowSiblingsChangedEvent = new SparseEvent();

        public event EventHandler TransitionCompleted
        {
            add { AddHandler(TransitionCompletedEvent, value); }
            remove { RemoveHandler(TransitionCompletedEvent, value); }
        }

        /// <summary>
        /// Occurs when <see cref="ShowSiblings"/> changes.
        /// </summary>
        public event EventHandler ShowSiblingsChanged
        {
            add { AddHandler(ShowSiblingsChangedEvent, value); }
            remove { RemoveHandler(ShowSiblingsChangedEvent, value); }
        }

        /// <summary>
        /// Occurs when the <see cref="P:SelectedItem"/> property changes.
        /// </summary>
        public event EventHandler SelectedItemChanged
        {
            add { AddHandler(ItemChangedEvent, value); }
            remove { RemoveHandler(ItemChangedEvent, value); }
        }

        /// <summary>
        /// Occurs when a value container control needs to be accessed.
        /// </summary>
        public event EventHandler<PickerItemEventArgs> GetVirtualItem;

        /// <summary>
        /// Occurs when a value container control is released from being used.
        /// </summary>
        public event EventHandler<PickerItemEventArgs> ReleaseVirtualItem;


        #endregion

    }
}
